#!/usr/bin/env python
## Generate a hosts file for Oracle RAC deployments
from optparse import OptionParser, make_option
from socket import gethostname, gethostbyname, gethostbyname_ex, socket, AF_INET, SOCK_STREAM, inet_ntoa
from string import Template
from shutil import copy2
from time import time
import fcntl, struct

## Math functions
def address_list(bin_addr = '11000000101010000000000100000001'): 
    '''Take a binary address and return a list of str(int(octet)) for each octet therein.'''    
    return [ str( int(octet, 2) ) for octet in ( bin_addr[:8], bin_addr[8:16], bin_addr[16:24], bin_addr[24:32] ) ]
    
def binary(n = 25): 
    '''Convert 'n' to binary since Python 2.4 can't on its own.'''
    n = int(n) 
    backwards_binary = ''
    while n > 0: 
        backwards_binary += str(n % 2)
        n //= 2
    return ( backwards_binary[::-1] ).rjust(8, '0')

def int_addr(ipaddr = '192.168.0.1'): 
    '''Convert dotted octets ipaddr into integer.'''
    return int( ''.join( [ binary(octet) for octet in ipaddr.split('.') ] ).rjust(32, '0'), 2 )
    
def ipaddr(i = '3232235777'): 
    '''Convert 'i', an ipaddr in integer form, to dotted octets.'''
    bin_ipaddr = binary( int(i) ).rjust(32, '0')
    return '.'.join( address_list(bin_ipaddr) )
    
def mask(prefix = 24): 
    '''Return netmask based on prefix.'''
    if '.' in str(prefix): 
        return prefix
        
    bin_mask = ''.ljust( int(prefix) , '1' ).ljust(32, '0') 
    return '.'.join(address_list(bin_mask)) 
    
def network(ipaddr, netmask = '255.255.252.0'): 
    '''Return the network address using the zipped octects of the ipaddr and netmask.'''
    if '/' in ipaddr: 
        ipaddr, prefix = ipaddr.split('/')
        netmask = mask(prefix)
    return '.'.join( [ str( int(i)&int(n) ) for i,n in zip( ipaddr.split('.'), netmask.split('.') ) ] )
    
def prefix(ipaddr = '', netmask = ''): 
    '''Generate prefix from netmask or ipaddr/prefix, if necessary for some reason'''
    if '/' in ipaddr: 
        netmask = ipaddr.split('/')[1]
        
    if '.' in netmask: 
        return str( ''.join( [ binary(octet) for octet in netmask.split('.') ] ).count('1') )
        
    return netmask
    
def prefixed_network(prefixed_ip = '66.154.99.178/26'): 
    ip, suffix = prefixed_ip.split('/')
    return network(prefixed_ip) + '/' + suffix
    
## gather options
def get_opts(): 
    '''Get cluster suffix, public IP/prefix, backup IP/prefix, interconnect IP/prefix, VLAN IDs or zone.'''
    option_list = [
        make_option('-d', '--dns', action = "store_true", default = True,  
            help = 'Use DNS from the local host to gather addresses.'
        ),
        make_option('-o', '--outfile', default = '',  
            help = 'Output to a file (it will be backed up).'
        ),
        make_option('-l', '--local', action = "store_true", default = True,  
            help = 'Use information from the local host to generate defaults.'
        ),
        make_option('-c', '--cluster', default = 'd06', 
            help = 'Cluster suffix, e.g., d06.'
        ),
       make_option('-p', '--public-ip', default = '10.11.12.13/22', 
            help = 'First host\'s public IP with prefix or netmask: 10.11.12.13/22 or 10.14.15.16/255.255.252.0'
        ),
        make_option('-b', '--backup-ip', default = '11.13.15.17/8', 
            help = 'First host\'s backup network IP with prefix or netmask: 11.13.15.17/17 or 11.13.15.17/255.255.128.0'
        ),
        make_option('-i', '--interconnect-ip', default = '192.168.0.1/26', 
            help = 'First host\'s RAC interconnect IP with prefix or netmask: 192.168.0.1/26 or 192.168.0.1/255.255.255.192'
        ),
        make_option('-n', '--nodes', default = 3, type = int,
            help = 'Number of nodes.'
        ), 
        make_option('-s', '--scan-ips', default = ['10.11.12.16', '10.11.12.17', '10.11.12.18'], type = 'string', 
            help = 'Space-delimited SCAN IPs, e.g., "10.11.12.16 10.11.12.17 10.11.12.18".'
        ),
         make_option('-v', '--virtual-ip', default = '10.11.12.16/22', 
            help = 'First host\'s virtual IP with prefix or netmask: 10.11.12.16/22 or 10.14.15.16/255.255.252.0'
        ), 
make_option('-z', '--zone', choices = ('int', 'ext'), default = 'int', 
            help = 'Shortcut to entering VLAN IDs: in external network, "ext" is (public, backup, IC) = (322, 901, 9999); internal "int" (public, backup, IC) = (907, 500, 9999)'
        ),
        make_option('-P', '--public-vlan',
            help = 'Public network VLAN ID (set automatically if zone is selected).'
        ),
        make_option('-B', '--backup-vlan',
            help = 'Backup network VLAN ID (set automatically if zone is selected).'
        ),
        make_option('-I', '--interconnect-vlan',
            help = 'Interconnect network VLAN ID (set automatically if zone is selected).'
        ),
    ]
    options, args = OptionParser(option_list=option_list, 
        usage = '%prog -c d06 -p 10.11.12.13/22 -b 11.13.15.17/8 -i 192.168.0.1/26 -z ext', 
        version = '%prog 0.5'
    ).parse_args()
    
    if (options.zone).lower().startswith('int'): 
        options.public_vlan, options.backup_vlan, options.interconnect_vlan = (
            options.public_vlan or '907', options.backup_vlan or '500', options.interconnect_vlan or '9999'
        )
    else: 
        options.public_vlan, options.backup_vlan, options.interconnect_vlan = (
            options.public_vlan or '322', options.backup_vlan or '901', options.interconnect_vlan or '9999'
        )

    if isinstance(options.scan_ips, str): options.scan_ips = sorted( (options.scan_ips).split() )

    # If being generated on a RAC node, use local information to fill options
    # The node needs to be operational for this and have its IPs and host name configured
    if options.local: 
        hostname = gethostname()

        options.cluster = hostname[-6:-3]
        options.scan_ips = sorted( gethostbyname_ex('orarac' + options.cluster)[-1] )
        options.public_ip = gethostbyname(hostname) + '/22'
        options.backup_ip = gethostbyname(hostname + 'p') + '/8'
        options.virtual_ip = gethostbyname(hostname + 'v') + '/22'
        # This may yet come in handy, although for now, it's deprecated
        # options.interconnect_ip = '192.168.0.' + hostname[-1] + '/26'
        s = socket(AF_INET, SOCK_STREAM)
        options.interconnect_ip = inet_ntoa(fcntl.ioctl(s.fileno(), 0x8915, struct.pack('256s', 'bond2'))[20:24]) + '/26'

    if options.backup_ip.startswith('11'): 
       options.zone = 'ext'
    else: 
	options.zone = 'int'

    return (options, args)
    
## formatting functions
def header(cluster = 'd06', scan_ips = ['10.11.12.16', '10.11.12.17', '10.11.12.18']): 
    line = '# /etc/hosts for orarac' + cluster + '\n127.0.0.1 localhost\n\n## SCAN IPs (see public range below for VLAN ID)\n'
    for each in scan_ips: 
        line += '\n# %s %s %s' % (each, 'orarac' + cluster + '.sempra.com', 'orarac' + cluster)
    return line
    #return '# /etc/hosts for orarac' + cluster + '\n127.0.0.1 localhost'
    
def body(cluster = 'd06', prefixed_ip = '10.11.12.13/22', nodes = 3, type = '', line_type = '', dns = 1): 
    contents = []
    if line_type == 'commented': 
        line = Template('# $ip orarac${cluster}-${node}${type}.sempra.com orarac${cluster}-${node}${type}')
    else: 
        line = Template('$ip orarac${cluster}-${node}${type}.sempra.com orarac${cluster}-${node}${type}')
    
    ip, suffix = prefixed_ip.split('/') 

    # If generated on other than the first RAC node, IPs will be off unless normalized
    # The + 1 is because the node numbering begins at 1
    ip = int_addr(ip) - int(gethostname()[-1]) + 1
    
    for each in range(nodes): 
        node = str(each + 1) 
        if each < 10: 
            node = '0' + node
        
        if dns and type != 'i': 
            contents.append(line.substitute(
                ip = gethostbyname( 'orarac' + cluster + '-' + node + type ), cluster = cluster, node = node, type = type
                )
            )
        else: 
            contents.append(line.substitute(
                ip = ipaddr(ip + each), cluster = cluster, node = node, type = type
                )
            )
        
    return '\n'.join(contents)
        
## main application
def main(): 
    '''Configure defaults and get options and fill out templated hosts file'''
    options, args = get_opts()
    
    hosts = Template('''${header}

## Public VLAN ${pub_vlan} (${pub_net})
${public_body}

## Virtual IP VLAN ${pub_vlan} (${pub_net})
${virtual_body}

## Backup VLAN ${bak_vlan} (${bak_net})
${backup_body}

## RAC IC VLAN ${rac_vlan} (${rac_net})
${rac_ic_body}\n'''
    )
    
    hosts_file = hosts.substitute(
        header = header(options.cluster, options.scan_ips), 
        
        pub_vlan = options.public_vlan, 
        pub_net = prefixed_network(options.public_ip),
        public_body = body(options.cluster, prefixed_ip = options.public_ip, nodes = options.nodes, dns = options.dns),
        virtual_body = body(
            options.cluster, prefixed_ip = options.virtual_ip, nodes = options.nodes, type = 'v', line_type = 'commented', dns = options.dns
        ),
        
        bak_vlan = options.backup_vlan,
        bak_net = prefixed_network(options.backup_ip),
        backup_body = body(
            options.cluster, prefixed_ip = options.backup_ip, nodes = options.nodes, type = 'p', dns = options.dns
        ),
        
        rac_vlan = options.interconnect_vlan,
        rac_net = prefixed_network(options.interconnect_ip),
        rac_ic_body = body(
            options.cluster, prefixed_ip = options.interconnect_ip, nodes = options.nodes, type = 'i', dns = options.dns
        )
    )
    
    if options.outfile: 
	try: 
	    copy2( options.outfile, options.outfile + '_' + str(time()) )
	except IOError: 
	    pass
	f = open(options.outfile, 'w')
	f.write(hosts_file)
	f.close()
    else: 
	print hosts_file

    return None
        
if __name__ == '__main__': 
    '''The last comment here was equally useful.'''
    main()
